package top.yueyazhui.module.rest_template.config;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
 
import javax.net.ssl.SSLContext;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.*;

/**
 * @description RestTemplate 配置
 * @author yueyazhui
 * @website yueyazhui.top
 * @email yueyazhui@sina.com
 * @date 2023-11-12 17:45
 */
@Slf4j
@Configuration
public class RestTemplateConfig {
 
    /* ---------- RestTemplate 相关参数 ---------- */
    @Value("${spring.rest-template.charset}")
    private String charset;
    @Value("${spring.rest-template.connect-timeout}")
    private int connectTimeout;
    @Value("${spring.rest-template.read-timeout}")
    private int readTimeout;
    @Value("${spring.rest-template.connection-request-timout}")
    private int connectionRequestTimeout;

    /* ---------- HttpClient 连接池相关参数 ---------- */
    @Value("${spring.rest-template.http-client.max-total}")
    private int maxTotal;
    @Value("${spring.rest-template.http-client.max-connect-per-route}")
    private int maxConnectPerRoute;
    @Value("${spring.rest-template.http-client.enable-retry}")
    private boolean enableRetry;
    @Value("${spring.rest-template.http-client.retry-times}")
    private int retryTimes;
    @Value("#{${spring.rest-template.http-client.keep-alive-target-hosts}}")
    private Map<String, Long> keepAliveTargetHosts;
    @Value("${spring.rest-template.http-client.keep-alive-time}")
    private long keepAliveTime;

    /**
     * 配置 RestTemplate
     */
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
        // 设置字符集编码 UTF-8；默认 ISO-8859-1
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> converter : messageConverters) {
            if (converter instanceof StringHttpMessageConverter) {
                ((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName(charset));
            }
        }
        // 设置自定义异常处理
        restTemplate.setErrorHandler(new MyErrorHandler());
        // 设置日志拦截器
        restTemplate.getInterceptors().add(new LoggingInterceptor());
        return restTemplate;
    }
 
    /**
     * 配置 HttpClient 工厂
     */
    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setHttpClient(httpClient);
        // 客户端和服务器建立连接的超时时间(ms)；最大20秒，因为tcp在进行三次握手建立连接时，默认的超时时间是20秒
        factory.setConnectTimeout(connectTimeout);
        // 客户端从服务器读取数据包的间隔超时时间(ms)；两个相邻数据包的间隔超时时间，即 socket timeout
        factory.setReadTimeout(readTimeout);
        // 连接不够用时从连接池获取连接的等待时间(ms)，必须设置；不宜过大，连接不够用时，等待时间过长将是灾难性的
        factory.setConnectionRequestTimeout(connectionRequestTimeout);
        // 必须使用 BufferingClientHttpRequestFactory 包装类，否则默认实现只允许读取一次响应流，日志输出那里读取之后，请求调用处再读取时会报错
        return new BufferingClientHttpRequestFactory(factory);
    }
 
    /**
     * 配置 HttpClient
     */
    @Bean
    public HttpClient httpClient(ConnectionKeepAliveStrategy connectionKeepAliveStrategy) {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        SSLContext sslContext = null;
        try {
            sslContext = SSLContexts.custom().loadTrustMaterial(null, (X509Certificate[] var1, String var2) -> true).build();
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            log.error(e.getMessage());
        }
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                // 注册 http 和 https 请求
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslConnectionSocketFactory)
                .build();
        // 使用 HttpClient 连接池的方式配置（推荐），同时支持 Netty、OkHttp 以及其他 Http 框架
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        // TCP 最大连接数
        poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
        // 每个路由的最大连接数（路由是指 IP + PORT）
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxConnectPerRoute);
        // 配置连接池
        httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
        // 是否允许重试和重试次数
        httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(retryTimes, enableRetry));
        // 设置长连接保持策略
        httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy);
        return httpClientBuilder.build();
    }
 
    /**
     * 配置长连接策略
     */
    @Bean
    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return (response, context) -> {
            HeaderElementIterator headerElementIterator = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            log.debug("HeaderElement:{}", headerElementIterator);
            while (headerElementIterator.hasNext()) {
                HeaderElement headerElement = headerElementIterator.nextElement();
                String name = headerElement.getName();
                String value = headerElement.getValue();
                if (StrUtil.isNotBlank(value) && "timeout".equalsIgnoreCase(name)) {
                    try {
                        // 1.服务器有时候会告诉客户端长连接的超时时间，如果有则设置为服务器的返回值
                        return Long.parseLong(value) * 1000;
                    } catch (NumberFormatException e) {
                        log.error("解析长连接超时时间异常", e);
                    }
                }
            }
            // 2.如果服务器没有返回超时时间则采用配置的时间。如果请求地址，单独配置了长连接超时时间，就使用该配置；否则使用默认的配置
            HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
            Optional<Map.Entry<String, Long>> any = Optional.ofNullable(keepAliveTargetHosts).orElseGet(HashMap::new)
                    .entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();
            return any.map(Map.Entry::getValue).orElse(keepAliveTime);
        };
    }
 
    /**
     * 自定义异常处理
     */
    private static class MyErrorHandler implements ResponseErrorHandler {
 
        @Override
        public boolean hasError(ClientHttpResponse response) throws IOException {
            return !response.getStatusCode().is2xxSuccessful();
        }
 
        @Override
        public void handleError(ClientHttpResponse response) throws IOException {
            // 响应状态码错误时抛出异常
            throw new HttpClientErrorException(response.getStatusCode(), response.getStatusText(), response.getHeaders(), IOUtils.toByteArray(response.getBody()), StandardCharsets.UTF_8);
        }
    }
 
    /**
     * 日志拦截器
     */
    private static class LoggingInterceptor implements ClientHttpRequestInterceptor {
 
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            traceRequest(request, body);
            long start = System.currentTimeMillis();
            ClientHttpResponse response = execution.execute(request, body);
            long end = System.currentTimeMillis();
            traceResponse(body, request, response, NumberUtil.sub(end, start));
            return response;
        }
 
        private void traceRequest(HttpRequest request, byte[] body) {
            String str =
                    "\n========== ========== ========== request begin ========== ========== ==========\n" +
                    "URI         : " + request.getURI() + "\n" +
                    "Method      : " + request.getMethod() + "\n" +
                    "Headers     : " + request.getHeaders() + "\n" +
                    "Request body: " + new String(body, StandardCharsets.UTF_8) +
                    "\n========== ========== ========== request end ========== ========== ==========\n";
            log.debug(str);
        }
 
        private void traceResponse(byte[] body, HttpRequest request, ClientHttpResponse response, double time) throws IOException {
            StringBuilder responseBody = new StringBuilder();
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
                String line = bufferedReader.readLine();
                while (line != null) {
                    responseBody.append(line);
                    line = bufferedReader.readLine();
                }
            }
            StringBuilder str = new StringBuilder();
            if (response.getStatusCode().is2xxSuccessful()) {
                str.append("\n========== ========== ========== response begin ========== ========== ==========\n")
                        .append("TIME         : ").append(time).append("ms").append("\n")
                        .append("URI          : ").append(request.getURI()).append("\n")
                        .append("Method       : ").append(request.getMethod()).append("\n")
                        .append("Status code  : ").append(response.getStatusCode()).append("\n")
                        .append("Headers      : ").append(response.getHeaders()).append("\n")
                        .append("Response body: ").append(responseBody)
                        .append("\n========== ========== ========== response end ========== ========== ==========\n");
                log.debug(str.toString());
            } else {
                str.append("\n========== ========== ========== response begin ========== ========== ==========\n")
                        .append("TIME         : ").append(time).append("ms").append("\n")
                        .append("URI          : ").append(request.getURI()).append("\n")
                        .append("Method       : ").append(request.getMethod()).append("\n")
                        .append("Headers      : ").append(request.getHeaders()).append("\n")
                        .append("Request body : ").append(new String(body, StandardCharsets.UTF_8)).append("\n")
                        .append("Status code  : ").append(response.getStatusCode()).append("\n")
                        .append("Headers      : ").append(response.getHeaders()).append("\n")
                        .append("Response body: ").append(responseBody)
                        .append("\n========== ========== ========== response end ========== ========== ==========\n");
                log.error(str.toString());
            }
        }
    }
}